home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Almathera Ten Pack 2: CDPD 1
/
Almathera Ten on Ten - Disc 2: CDPD 1.iso
/
pd
/
076-100
/
084
/
audiotools
/
original.article.text
< prev
next >
Wrap
Text File
|
1995-03-13
|
19KB
|
426 lines
The Audio Device On The Amiga
-----------------------------
May, 1986, somebody asks: "Why couldn't they have made audio easier to use?"
Is it really so difficult, I ask myself. After all, it seems pretty
thoroughly documented. But then again, why are so few people using
audio so far if it is supposed to be so good on the Amiga. But I'm
really busy trying to document other things so I leave audio to the
experts. As help is requested, it appears that the designer of the
audio device is advising people in person. I guess that whoever
needs help is getting directly to him.
Fast forward to November, 1986. I am not at Amiga any more.
I'm consulting for a couple of other places and doing the Programmers'
Guide To The Amiga in whatever spare time I have left. Comes the
time to do an audio chapter. Hmmm, the ROM Kernel stuff
describes a lot of features, but demonstrates only a few of them.
And it goes directly to the hardware. I don't want to do that!
My editor recommends another Amiga book that seems to have an "inside look"
at the audio. So I got it and, oh no, it goes directly to the hardware
too! What I want to do is queue up several sounds, and have
the the audio system play them sequentially while my task goes on to do
something else. Going directly to the hardware meant the audio device
will neither count cycles for me nor queue sounds for automatic
play.
Sound queueing could be done using the audio device command
called CMD_WRITE, but at first I found no examples that used the
CMD_WRITE command. Later, after several false starts (and after
completing the audio chapter and most of the rest of the book) I
finally discovered an example program on Usenet (reposted from BIX)
that used CMD_WRITE. Some pieces of that program, along with what
I developed subsequent to the audio chapter, appear here.
I simply wanted my main program to look like this:
main()
{
/* ... (program-stuff) ... */
InitAudio(); /* initialize everything */
channel = GetChannel(-1);
if(channel != -1)
{
PlayNote(channel, waveform, note_no, volume, duration);
/* ... (more PlayNotes) ... */
}
/* do non-audio things here */
FreeChannel(channel);
FinishAudio(); /* close everything down */
}
THAT seemed to make it simple, and it seemed to be what I wanted (and what
others had been asking for). The details of device access and message passing
are buried in a subroutine somewhere, where a person need not deal with it.
After I created some of these routines I tested them on a few cases and
they seem to be what people wanted, so here they are.
The functions you see above are provided in
this article, with other support functions for the audio device.
By examining the source code I've provided, you will see
just how to communicate with the audio device and you
may be able to add your own enhancements to these techniques.
At the end of the article, you'll find a list of some of the
enhancements I expect to install by the time this article actually
appears in print, as well as other features that people have requested.
Note: If not otherwise stated, all parameters passed to the routines and
passed back as return values are LONG integers (32-bits). Sometimes
a pointer (also 32-bits) is used, and is shown as such.
The Functions, Explained
------------------------
1. gotchannel = GetChannel(channel);
channel - any number from 0 to 3, corresponding to a specific
hardware channel on the Amiga. If you ask for channel
number -1, it means get ANY channel that is available.
The function GetChannel returns the channel number, or
returns -1 if none are available to you.
2. PlayNote(channel,waveform,note_no,volume,duration,priority,message);
channel - a channel that you already own. If you
don't own it, the note simply will not play.
waveform - a pointer to the start of a waveform table
that contains 256 samples of a single wave
of your sound. Sample values range from
-128 to +127. The waveform table also includes
copies of the same waveform, each having fewer
and fewer samples in the table (128 samples,
64, 32 and so on).
This waveform table lets us stay within
the allowable limits of the Amiga audio hardware.
In particular, period values of 127 through 500
are the values that lets the Amiga output the
best quality audio. To get an output that is
of a high frequency, since the period values
are limited, each wave of the waveform must be
output more quickly. Thus the table with
several copies of the waveform, each having
different numbers of samples. See the source
code for MakeWaves to see how the tables are built.
Note Number - Notes are numbered from 0 to 95, structured
as 8 octaves of 12 notes each. Each octave has
its own waveform table entry having a length
appropriate to that octave.
Volume - Takes a value from 0 to 64 where 0 is minimum.
Duration - specified in 1000ths of a second. Five Hundred
thousandths of a second, for example, is one-half
of a second. The audio device accepts a command
to output a specific number of cycles of a waveform.
I calculate the frequency (in cycles per second)
from the note number, then multiply by Duration
and divide by 1000, yielding the correct number
of cycles for that frequency. Thus all notes
play for the correct time.
Priority - [NOT IMPLEMENTED YET]. If priority is 0,
just queue the note. If less than 0, flush all
current requests for this channel and start this
note only. If greater than 0, do not flush...
the priority value is only going to be used to
identify the note number to you when the note
begins to play.
Message - [NOT IMPLEMENTED YET]. Audiotools can send
you a message that contains an identifier of
your choice (the priority value) to let your
task know that this note has just begun to
play. On receiving the message, your task
must reply to it so that the audiotools can
reuse or deallocate the message memory.
3. FreeChannel(channel) - frees a channel that you own to let another task
(or your own task, later) use the channel.
4. InitAudio()
5. FinishAudio() - These functions take care of the background work,
such as opening and closing the audio device.
By using these routines, you do not have to deal with the audio device
at all. You need not allocate and initialize message blocks and so on.
All of this is built into the support routines and associated global variables.
By using the routines, though, you add some additional overhead to
accessing the audio device. If you are designing a high performance
audio routine, you just may have to lock the channels and go directly
to the hardware. For that kind of thing, you have the ROM Kernel
examples to guide you.
But for the rest of us, who just need to BEEP at somebody, these
routines make the access just a bit simpler (and provide a
functional jumping off point for further audio development).
You could also get rid of the subroutine call overhead by copying
appropriate portions of code directly into your main program.
Note: PlayNote is asynchronous. This means that it queues up a note to be
played by the audio device and then returns to the calling program
immediately. (It does not wait for the note to be finished before
it returns to the caller). ALL other functions in this article
are synchronous. That is, the function is performed entirely before
your program goes on to do something else.
What The Sample (main) Program Does
-----------------------------------
Using the above functions, main simply plays a few notes through each
channel, in each of the waveforms: sawtooth, triangle and square waves.
All 4 channels are active at the same time. When all notes have completed,
the program exits.
Support Functions
-----------------
This audio library has the following support functions.
6. error = StopChannel(channel)
7. error = StartChannel(channel) - stop or start a specific channel.
If a CMD_WRITE arrives at a stopped channel, it queues
and waits for the channel to be started. A return value
of 0 means no error. A return of -1 indicates low memory.
Any other value is a direct return from io_Error. See
devices/audio.h for meanings of other return values.
StopChannel terminates any CMD_WRITE currently in progress.
8. error = FlushChannel(channel) - If there are CMD_WRITE's lined up
to be played, return them all to the caller (flush input).
9. error = ResetChannel(channel) - reset it to its default values.
Also means flush a channel's input queue.
10. stereopair = GetStereoPair(pair) -
In the Amiga hardware, audio channels 0 and 3 are connected
to the Left audio output; audio channels 1 and 2 are connected
to the Right audio output. Thus to get a stereo pair, you
need left-channel and one right-channel.
Specify pair as -1 for ANY stereo pair,
or specify 0 (for audio channels 0 and 1), 1 (for 0 and 2),
2 (for 1 and 3) or 3 (for 2 and 3). Return value is the pair
that was available, or -1 if no stereo pair is available.
FreeChannel(stereopair) works just as FreeChannel(channel).
11. error = SetPV(channel, period, volume) - set the period and volume of
a note that is playing currently. Note that there is only
a limited range available for the period (roughly 127 to 500)
so is it more likely that you would use PlayNote instead since
PlayNote can modify the waveform pointer as well as the other
parameters.
Internal Functions
------------------
The following internal functions are used by the library functions
shown earlier. Your programs may, at times, need these functions as well.
These functions create, initialize and free audio device message blocks.
(I call them IOBs, for I/O Blocks).
12. iob = GetIOB() - allocate or assign an IOAudio structure for use.
Returns a value of 0 if system is too low on memory.
If no IOB is available from a specified pool of IOB's,
then dynamically allocate an IOB and pass back its address.
Note: for more advanced system functions suggested by the users
group members (shown later in this article), this structure
may need to be extended to hold additional parameters.
For now, though, the ExtIOB structure is identical
to the normal IOAudio structure (created for now by a define
statement). This allows us to define an extended version
of the structure later, with little if any change to
existing functions.
13. ReEmployIOB() - look at audio channel reply ports and see if any
IOB's have returned (are now unemployed) and can therefore
be reassigned or deallocated.
14. FreeIOB() - return a finished IOB to the free IOB pool or deallocate
a dynamically allocated one.
15. InitBlock(iob,channel) - Initialize an IOB for communication with a
specific channel, default command is CMD_WRITE. iob is a pointer
to an IOAudio structure. Channel is the specific channel for
which this block is to be initialized (allocation key is the
critical item).
16. ExpandWave(waveform_pointer) - Takes a pointer to a waveform buffer
that contains one cycle of a waveform, in 256 consecutive
bytes, and expands the table to add the same waveform sampled
128 times at twice the sampling interval, 64 times at 4 times
the sampling interval, 32 times/16 times/8 times and so on.
NOTE: the wave tables MUST be in CHIP memory otherwise
the audio device will be unable to play the notes!!!
ExpandWave is associated with MakeWaves() that creates three
tables total, one containing a sawtooth wave, one a triangle
wave, and the third contains a square wave. ExpandWave
completes the table entries for each waveform.
All of the waveforms are left in contiguous memory after the
first wave, in order of decending sizes (256, 128, 64, ...)
MakeWaves, ExpandWave, SetPV and PlayNote are paraphrased versions of
similar routines found in a posting to BIX by Steven A. Bennett.
Thanks, Steven, for the inspiration on this project. Steven's posted
article also provided the waveform and period tables I've used, as
well as the excellent explanation of the period value calculation
that I've quoted (slightly modified) below.
As you examine the source code provided, you'll see that the
audio device requires a period value rather than a frequency value.
The period table contains the period value corresponding to the
frequencies of the normal scale (12 notes per octave... see ABasiC
manual, page 138). You could calculate period yourself from the
formula:
period = Clock / ( samples-per-wave * frequency)
Clock rate is 3,579,545 cycles per second
So if you are playing a wave table that contains 32 samples, and
your selected output frequency is to be middle-A (440 hz),
of the piano the period value must be
3579545 / (32 * 440) = 254.229.
<SAB>: "But the audio device only accepts a whole number, so your result
has to be rounded down to the nearest integer which can result
in a maximum frequency error of about .25%, assuming one uses the
octave for frequency between period 226 and period 428. (This comes
out to be less than a twentieth step at the shortest period)."
<SAB>: "Period values of less than 127 are illegal, as there aren't enough
cycles set aside for audio DMA for anything less. period values of
greater than 500 or so aren't recommended as the anti-aliasing
filter isn't of much use then, and actually could cause a possible
high pitched overtone, which I'm sure nobody wants. Thus I am
only going to use SetPV to handle a single octave's range."
<SAB>: "Changes of octave are accomplished by doubling or halving the
number of samples in one cycle of the waveform, so one must, therefore,
call PlayNote() instead of SetPV()."
Additional Information About Internal Functions
-----------------------------------------------
For GetIOB, you can control how many structures are allocated for
IOAudio use. How many audio ioblocks should the system have available for
queueing up notes? If you want to queue up a whole song
by using a whole bunch of PlayNote commands and go away to do
something else, it could take a lot of memory! Once the system
runs out of these preallocated structures, it must dynamically
allocate and free memory... this can cause fragmentation of memory space.
You might want to send parts of the song at a time instead of the
whole song.
Depending on the variable AUDBUFFERS, defined when the
program is compiled, GetIOB either returns the address of
a buffer in global memory space, named "global"
(in the name field of the I/O message, node area) or
named "dynamic" if GetIOB runs out of AUDBUFFERS global blocks
to use. The number of dynamic blocks is limited only by the
available system memory (FAST memory, that is non-CHIP memory
is used for the I/O blocks).
NOTE: To be able to use only a standard sized IOAudio structure
for the message passing, I assigned the message mn_Length field
to identify the global blocks. (As of 1.1 and 1.2, the mn_Length
field is still available for anybody to decide what meaning it has).
To be perfectly safe, as well as to handle the advanced functions
that people have requested, an extended audio block should probably be
used... with a LONG quantity appended to it as the identifier
in place of using mn_Length, as well as a few other fields. This
change is very likely to be made for the disk version of the tools.
(The structure ExtIOB will be used as an extended version of IOAudio).
The Audio Tools And A Test Program
----------------------------------
Here are the listings that implement the tools described above.
Following the listings are some improvements that have been
suggested. Whatever I've been able to implement of those
improvements, as well as this code, appears on the disk mentioned
at the end of the article.
I hope these help you to better understand the audio system of
the Amiga.
========================================
<Insert All Listings Here>
========================================
What People Have Suggested
--------------------------
I did a chalk-talk at a developers' group meeting, and showed them what
I was working on for this article. They suggested the following additions
to what you see described above.
0. Add examples that use the stereo pair; check that
all error conditions are properly reported ("bullet-proofing").
(This is numbered "0" because it just has-to-be-done).
1. Implement the Priority and Message fields of PlayNote,
just as described.
2. Add a PlaySong function that can take a pointer
to a data structure that describes a song, with
some of the parameters that PlayNote takes, and
just play the song automatically.
3. Add a PlayWave function to handle sampled sounds, such as:
PlayWave(channel,sample_addr,copy,period,repeats,priority,message)
Global variables would be expanded to include a separate
ReplyPort for the sampled sounds. A copy (TRUE/FALSE)
parameter would specify (if TRUE) that the sampled_wave should be
copied (into chip memory) before queueing it to be played.
(Only CHIP memory waveforms can be played anyway).
If FALSE, it would assume that sample_addr is in chip
memory and that you will not change the contents of memory
before the note has completed playing.
4. Add a PlayFreq function that takes a frequency value instead
of a note number so that oriental music, for example, not
based on the same scale we use for a piano, could be played.
PlayFreq would take exactly the same parameters as PlayNote
but substitute "frequency" for "note_no". It would calculate
which is the longest waveform that can be used for the
selected frequency and still leave the period value within
the appropriate range of 127 to 500.
5. Add an implied-rest between notes so that it would sound
more natural and avoid having to explicitly encode such
rests into a song structure.
6. Add the ability to specify a slew rate for either volume
or frequency or both so that notes instead of going
directly from one setting to another can slide to
the new setting at a specified rate. Or perhaps
better still, add full ADSR capabilities.
This last one is a little tricky. It could require
software interrupts or perhaps even breaking into the
audio interrupt vector itself. It will also require
a data structure larger than the basic IOAudio structure
to hold these new variables.
This article is basically a report on current progress on a continuing
project whose goal is to develop a freely distributable set of license-free
routines that make it easier to use Amiga audio. I welcome suggestions
as to additional enhancements that might be useful, or code samples
that implement such enhancements.